﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.Remoting.Channels.Ipc;
using System.IO;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting;
using RoketPack.Management;
using System.Security.Cryptography;
using System.Net;
using System.Drawing;

namespace RoketPack
{
    /// <summary>
    /// This class runs in the background constantly and is used by the UI to manage
    /// application updates and repositories.  It also queries repositories for new
    /// updates every hour.
    /// </summary>
    public class Manager : MarshalByRefObject
    {
        private NotifyIcon m_SystemTrayIcon;
        private ContextMenu m_ContextMenu;
        private Timer m_UpdateTimer;
        private AppDomain m_ServerDomain;
        private List<Repository> p_Repositories;
        private bool p_RepositoriesInitialized = false;
        private MainForm p_MainForm;
        private SettingsForm m_SettingsForm;
        private IM.Manager p_IMManager;
        private static Encoding p_Encoding = Encoding.GetEncoding("ISO-8859-1");
        private static string p_OfficialRepository = "https://pack.roket-enterprises.com";
        private static string p_RoketPackID = "roketpack";
        private API.InterProcess m_InterProcess;
        private GlobalSettings p_Settings = null;
        private Icon m_SmallIcon = Properties.Resources.RoketPackSysTrayIconDark;
        private Icon m_FormIcon = Properties.Resources.RoketPackFormIconLight;
        internal delegate void VoidLambda();
        internal delegate void VoidLambdaDouble(double num);

        public Manager()
        {
            try
            {
                // Attempt to start the server, or close
                // the application if it's already running.
                if (!this.AttemptServerSetup("roketpack_" + Environment.UserName))
                {
                    // The server is already running.  Close
                    // the application down.
                    throw new ImmediateExitException();
                }
            }
            catch (FileLoadException)
            {
                // No API available.
            }

            // Ensure that the X.509 certificate is currently
            // installed on the user's machine.
            if (!Certificate.EnsureInstalled())
            {
                // The user either declined the installation or
                // the certificate could not otherwise be installed.
                throw new ImmediateExitException();
            }

            // Load the global settings.
            this.InitalizeGlobalSettings();

            // Set up the system tray.
            this.InitalizeSystemTray();

            // Set up the update timer.
            this.InitalizeUpdateTimer();

            // Set up the list of repository objects.
            this.InitalizeRepositories();

            // Set up the main form (hidden).
            this.InitalizeMainForm();

            // Set up the friends list and IM connection.
            this.InitalizeIM();
        }

        private void InitalizeIM()
        {
            this.p_IMManager = new IM.Manager(this);
            this.p_IMManager.Start();
        }

        internal List<Repository> Repositories
        {
            get
            {
                return this.p_Repositories;
            }
        }

        internal bool RepositoriesInitialized
        {
            get
            {
                return this.p_RepositoriesInitialized;
            }
        }

        internal MainForm MainForm
        {
            get
            {
                return this.p_MainForm;
            }
        }

        internal GlobalSettings Settings
        {
            get
            {
                return this.p_Settings;
            }
        }

        internal IM.Manager IMManager
        {
            get
            {
                return this.p_IMManager;
            }
        }

        internal event EventHandler RepositoriesRefreshed;
        protected virtual void OnRepositoriesRefreshed()
        {
            // Search for new updates.
            bool updates = false;
            foreach (Repository r in this.p_Repositories)
            {
                // Ensure the repository is loaded correctly.
                if (r.Applications == null)
                    continue;

                foreach (RoketPack.Management.Application a in r.Applications.Values)
                {
                    foreach (Component c in a.Components.Values)
                    {
                        if (c.IsOutOfDate() && c.IsInstalled())
                        {
                            updates = true;
                            break;
                        }
                    }
                }
            }
            if (updates)
                this.m_SystemTrayIcon.ShowBalloonTip(300,
                    "Updates Available",
                    "There are updates available for your software.",
                    ToolTipIcon.Info);

            this.RepositoriesRefreshed(this, new EventArgs());
        }

        /// <summary>
        /// Returns a new IconAnimator with the correct system tray set in the constructor.
        /// </summary>
        /// <param name="icons">The icons to rotate through.</param>
        /// <param name="defaulticon">The icon to set the system tray back to after Stop() is called.</param>
        /// <param name="interval">The interval between switching images.</param>
        /// <returns>A new IconAnimator.</returns>
        internal IconAnimator GetIconAnimator(List<Icon> icons, Icon defaulticon, int interval)
        {
            return new IconAnimator(this.m_SystemTrayIcon, icons, defaulticon, interval);
        }

        /// <summary>
        /// Hides the system tray icon on behalf of another object (since m_SystemTrayIcon
        /// is private to this class and is not exposed as a property).  Used to hide the
        /// system tray icon before the application is closed via Application.Exit().
        /// </summary>
        internal void SystemIconHide()
        {
            this.m_SystemTrayIcon.Visible = false;
        }

        /// <summary>
        /// Shows a balloon tool tip on the system tray icon on behalf of another object (since
        /// m_SystemTrayIcon is private to this class and is not exposed as a property).
        /// </summary>
        /// <param name="timeout">The timeout for the balloon tip.</param>
        /// <param name="tipTitle">The balloon title.</param>
        /// <param name="tipText">The text on the balloon.</param>
        /// <param name="tipIcon">The icon to use when showing the balloon.</param>
        internal void SystemIconShowBalloon(int timeout, string tipTitle, string tipText, ToolTipIcon tipIcon)
        {
            this.m_SystemTrayIcon.ShowBalloonTip(timeout, tipTitle, tipText, tipIcon);
        }

        /// <summary>
        /// Initializes the main form so that it is invisible and hidden (but
        /// available for function calls.
        /// </summary>
        internal void InitalizeMainForm()
        {
            if (this.p_MainForm == null)
            {
                this.p_MainForm = new MainForm(this);
                this.p_MainForm.Disposed += (dsender, de) => { this.p_MainForm = null; };
                this.p_MainForm.StartPosition = FormStartPosition.CenterScreen;
                IntPtr h = this.p_MainForm.Handle; // Create the handle.
                this.p_MainForm.Hide();
            }
        }

        /// <summary>
        /// Initializes the global settings object from file.
        /// </summary>
        private void InitalizeGlobalSettings()
        {
            this.p_Settings = GlobalSettings.Load();
        }

        /// <summary>
        /// Initializes the repositories from the Repositories.xml file located
        /// in the current working directory.
        /// </summary>
        private void InitalizeRepositories()
        {
            this.InitalizeRepositories(false);
        }

        /// <summary>
        /// Initalizes the repositories from the Repositories.xml file located
        /// in the current working directory.
        /// </summary>
        /// <param name="refresh">Indicates whether the application data should be refreshed.</param>
        /// <remarks>
        /// This function retrieves information about what applications are available from each
        /// repository (i.e. it pulls down the software.xml file).  It doesn't actually fetch the
        /// application XML files (those files that are linked by software.xml), unless you specify
        /// the refresh argument set to true.
        /// </remarks>
        private void InitalizeRepositories(bool refresh)
        {
            // Disable the settings option in the context menu.
            this.m_ContextMenu.MenuItems[2].Enabled = false;
            this.p_RepositoriesInitialized = false;

            IconAnimator icon = new IconAnimator(
                this.m_SystemTrayIcon,
                IconAnimator.UpdateSet,
                this.m_SmallIcon,
                100);

            ConfigurationReader config = new ConfigurationReader();
            this.p_Repositories = config.GetRepositoryList();

            System.Threading.ThreadStart lambda = () =>
                {
                    try
                    {
                        List<Repository> failed = new List<Repository>();
                        foreach (Repository r in this.p_Repositories)
                        {
                            try
                            {
                                r.Initalize();

                                // Ensure the repository is loaded correctly.
                                if (r.Applications == null)
                                    continue;

                                foreach (RoketPack.Management.Application a in r.Applications.Values)
                                    a.SyncCache();
                            }
                            catch (CryptographicException ex)
                            {
                                // Could not establish a secure SSL connection.
                                failed.Add(r);
                            }
                            catch (WebException ex)
                            {
                                // Could not otherwise retrieve the repository file (i.e. 404).
                                failed.Add(r);
                            }
                        }

                        // Show an error message through the system tray icon if the length
                        // of failed is greater than 0.
                        if (failed.Count > 0)
                        {
                            string start = "Some software repositories could not be contacted:\n\n";
                            string end = "\nSome software may not be available.";
                            string msg = start;
                            int charcount = start.Length + end.Length;
                            foreach (Repository r in failed)
                            {
                                if (charcount + ("  - " + r.Name + "\n").Length > 255 - 12)
                                {
                                    msg += "  <more...>\n";
                                    break;
                                }
                                msg += "  - " + r.Name + "\n";
                                charcount += ("  - " + r.Name + "\n").Length;
                            }
                            msg += end;

                            this.m_SystemTrayIcon.ShowBalloonTip(1500, "Contacting Repositories Failed", msg, ToolTipIcon.Error);
                        }

                        icon.Stop();
                        this.p_RepositoriesInitialized = true;

                        if (this.p_MainForm != null)
                        {
                            VoidLambda refreshl = () =>
                            {
                                this.p_MainForm.InitializeList();
                                this.OnRepositoriesRefreshed();
                            };

                            if (this.p_MainForm != null)
                            {
                                if (this.p_MainForm.InvokeRequired)
                                    this.p_MainForm.Invoke(refreshl);
                                else
                                    refreshl();
                            }
                        }

                        if (refresh)
                        {
                            VoidLambda refreshl = () =>
                            {
                                if (this.p_MainForm != null)
                                    this.p_MainForm.c_RefreshInformationToolStripButton.PerformClick();
                                else
                                    this.RefreshRepositories();
                            };

                            if (this.p_MainForm != null)
                            {
                                if (this.p_MainForm.InvokeRequired)
                                    this.p_MainForm.Invoke(refreshl);
                                else
                                    refreshl();
                            }
                        }

                        // Re-enable the settings option in the context menu.
                        this.m_ContextMenu.MenuItems[2].Enabled = true;
                    }
                    catch (Exception ex)
                    {
                        Program.HandleUnhandledException(ex);
                        System.Windows.Forms.Application.Exit();
                    }
                };

            System.Threading.Thread thread = new System.Threading.Thread(lambda);
            thread.Name = "Repository Initialization Thread";
            thread.IsBackground = true;
            thread.Start();
        }

        /// <summary>
        /// Initializes the update timer so that it will periodically check for updates
        /// from the repositories.
        /// </summary>
        private void InitalizeUpdateTimer()
        {
            this.m_UpdateTimer = new Timer();
            this.m_UpdateTimer.Interval = 60 * 60 * 1000;
            this.m_UpdateTimer.Enabled = true;
            this.m_UpdateTimer.Tick += new EventHandler(OnPeriodicUpdate);
        }

        /// <summary>
        /// Checks for new updates to software.  This function is the periodic update
        /// handler for m_UpdateTimer.
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The event arguments.</param>
        private void OnPeriodicUpdate(object sender, EventArgs e)
        {
            if (!this.p_RepositoriesInitialized)
                return;

            if (this.p_MainForm != null)
                this.p_MainForm.c_RefreshInformationToolStripButton.PerformClick();
            else
                this.RefreshRepositories();
        }

        /// <summary>
        /// Initializes the system tray icon so that it is visible in the task tray.
        /// </summary>
        private void InitalizeSystemTray()
        {
            this.InitalizeContextMenu();

            if (Aero.Enabled)
            {
                this.m_SmallIcon = Properties.Resources.RoketPackSysTrayIconLight;
                this.m_FormIcon = Properties.Resources.RoketPackFormIconDark;
            }

            this.m_SystemTrayIcon = new NotifyIcon();
            this.m_SystemTrayIcon.Icon = this.m_SmallIcon;
            this.m_SystemTrayIcon.Text = "RoketPack";
            this.m_SystemTrayIcon.ContextMenu = this.m_ContextMenu;
            this.m_SystemTrayIcon.Visible = true;
            this.m_SystemTrayIcon.DoubleClick += this.OpenUI;
        }

        internal Icon SmallIcon
        {
            get
            {
                return this.m_SmallIcon;
            }
        }

        internal Icon FormIcon
        {
            get
            {
                return this.m_FormIcon;
            }
        }

        /// <summary>
        /// Initializes the m_ContextMenu object to contain the default set
        /// of menu items.
        /// </summary>
        private void InitalizeContextMenu()
        {
            this.m_ContextMenu = new ContextMenu();
            this.m_ContextMenu.MenuItems.Add(
                new MenuItem("Software Manager", this.OpenUI)
                );
            this.m_ContextMenu.MenuItems.Add(
                new MenuItem("Friends List", this.OpenFriendList)
                );
            this.m_ContextMenu.MenuItems.Add(
                new MenuItem("-")
                );
            this.m_ContextMenu.MenuItems.Add(
                new MenuItem("Settings", this.OpenSettings)
                );
            this.m_ContextMenu.MenuItems.Add(
                new MenuItem("Exit", this.Shutdown)
                );
            this.m_ContextMenu.Popup += new EventHandler((sender, e) =>
                {
                    // We need to clear any IM notifications when
                    // the icon is clicked to prevent weird
                    // graphical glitches.
                    this.p_IMManager.ClearNotifications();
                });

            // Disable settings during startup.
            this.m_ContextMenu.MenuItems[2].Enabled = false;
        }

        /// <summary>
        /// Opens the main UI form (as an event handler).
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The event arguments.</param>
        private void OpenUI(object sender, EventArgs e)
        {
            if (this.p_MainForm == null)
            {
                this.p_MainForm = new MainForm(this);
                this.p_MainForm.Disposed += (dsender, de) => { this.p_MainForm = null; };
                this.p_MainForm.StartPosition = FormStartPosition.CenterScreen;
                IntPtr h = this.p_MainForm.Handle; // Create the handle.
                this.p_MainForm.Show();
            }
            else
                this.p_MainForm.Show();

            this.p_MainForm.TopMost = true;
            this.p_MainForm.Focus();
            this.p_MainForm.BringToFront();
            this.p_MainForm.TopMost = false;
        }

        /// <summary>
        /// Opens the IM friends list form (as an event handler).
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The event arguments.</param>
        private void OpenFriendList(object sender, EventArgs e)
        {
            if (!this.p_IMManager.Visible)
                this.p_IMManager.Show();

            this.p_IMManager.Form.TopMost = true;
            this.p_IMManager.Form.Focus();
            this.p_IMManager.Form.BringToFront();
            this.p_IMManager.Form.TopMost = false;
        }

        /// <summary>
        /// Opens the settings form (as an event handler).
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The event arguments.</param>
        internal void OpenSettings(object sender, EventArgs e)
        {
            if (this.m_SettingsForm == null)
            {
                this.m_SettingsForm = new SettingsForm(this);
                this.m_SettingsForm.Disposed += (dsender, de) => { this.m_SettingsForm = null; };
                this.m_SettingsForm.StartPosition = FormStartPosition.CenterScreen;
                IntPtr h = this.m_SettingsForm.Handle; // Create the handle.
                this.m_SettingsForm.Show();
            }
            else
                this.m_SettingsForm.Show();
        }

        /// <summary>
        /// Shuts down the software management system entirely (as an event handler).
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The event arguments.</param>
        private void Shutdown(object sender, EventArgs e)
        {
            this.m_SystemTrayIcon.Visible = false;
            System.Windows.Forms.Application.Exit();
        }

        /// <summary>
        /// Attempts to set up the IPC server channel so that other instances of RoketPack
        /// can communicate and send requests to the server.  Returns false if the application
        /// should exit because another instance is already running.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private bool AttemptServerSetup(string name)
        {
            try
            {
                // Set up an interprocess object which will handle instructions
                // from the client and pass them onto the main Manager object.
                this.m_ServerDomain = AppDomain.CreateDomain("roketpack_server");
                this.m_ServerDomain.DoCallBack(() =>
                    {
                        BinaryServerFormatterSinkProvider serverProv = new BinaryServerFormatterSinkProvider();
                        serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

                        IpcServerChannel ipc = new IpcServerChannel("roketpack_" + Environment.UserName, "roketpack_" + Environment.UserName, serverProv);
                        ChannelServices.RegisterChannel(ipc, false);

                        RemotingConfiguration.RegisterWellKnownServiceType(
                            typeof(API.InterProcess),
                            "InterProcessManager",
                            WellKnownObjectMode.Singleton);
                    });

                // Now initialize the object.
                IpcClientChannel client = new IpcClientChannel();
                ChannelServices.RegisterChannel(client, true);
                this.m_InterProcess = (API.InterProcess)Activator.GetObject(
                    typeof(API.InterProcess),
                    "ipc://" + name + "/InterProcessManager");
                this.m_InterProcess.SetManager(this);
                this.m_InterProcess.SetCalls(
                    InterProcessHandle.ApplicationIsInstalled,
                    InterProcessHandle.ComponentIsInstalled,
                    InterProcessHandle.ComponentInstall,
                    InterProcessHandle.ComponentUpdate,
                    InterProcessHandle.ComponentUninstall,
                    InterProcessHandle.VersionIsLatest,
                    InterProcessHandle.MiscellaneousCallURL,
                    InterProcessHandle.AuthorizedCallURL);

                return true;
            }
            catch (RemotingException)
            {
                // The server appears to be already running.  Connect to
                // the channel as a client and send instructions back
                // to the server.
                IpcClientChannel client = new IpcClientChannel();
                ChannelServices.RegisterChannel(client, true);

                API.InterProcess i = (API.InterProcess)Activator.GetObject(
                    typeof(API.InterProcess),
                    "ipc://" + name + "/InterProcessManager");

                if (i == null)
                {
                    Errors.Raise(Errors.ErrorType.ERROR_CAN_NOT_START_OR_CONNECT_TO_IPC);
                    return false;
                }

                if (Environment.GetCommandLineArgs().Length > 1)
                    i.AuthorizedCallURL(Environment.GetCommandLineArgs()[1]);

                return false;
            }
        }

        /// <summary>
        /// Updates all of the information for all of the applications with new information
        /// from the remote repositories.  This is an asynchronous operation, and so it
        /// returns immediately.
        /// </summary>
        /// <remarks>
        /// This function retrieves new information about the application that RoketPack
        /// already knows about; for example, it will retrieve new information about
        /// existing applications, but it will not be able to retrieve information about
        /// applications that previously weren't in the repository because it does not
        /// pull down a new copy of software.xml.
        /// </remarks>
        internal void RefreshRepositories()
        {
            System.Threading.ThreadStart lambda = () =>
            {
                try
                {
                    IconAnimator animator = new IconAnimator(this.m_SystemTrayIcon, IconAnimator.UpdateSet,
                    this.m_SmallIcon, 100);

                    foreach (Repository r in this.p_Repositories)
                    {
                        // Ensure that the repository is loaded correctly.
                        if (r.Applications == null)
                            continue;

                        foreach (RoketPack.Management.Application a in r.Applications.Values)
                        {
                            Cache.FetchApplicationData(a);
                        }
                    }

                    VoidLambda refresh = () =>
                    {
                        this.p_MainForm.InitializeList();
                        animator.Stop();
                        this.OnRepositoriesRefreshed();
                    };

                    if (this.p_MainForm != null)
                    {
                        if (this.p_MainForm.InvokeRequired)
                            this.p_MainForm.Invoke(refresh);
                        else
                            refresh();
                    }
                }
                catch (Exception ex)
                {
                    Program.HandleUnhandledException(ex);
                    System.Windows.Forms.Application.Exit();
                }
            };

            System.Threading.Thread upthread = new System.Threading.Thread(lambda);
            upthread.Name = "Repository Refresh Thread";
            upthread.IsBackground = true;
            upthread.Start();
        }

        /// <summary>
        /// Returns the encoding to use when writing out downloaded XML data.
        /// </summary>
        internal static Encoding Encoding
        {
            get
            {
                return Manager.p_Encoding;
            }
        }

        /// <summary>
        /// Returns the official repository string to be used in checking whether
        /// a repository is the official repo run by Roket Enterprises (and contains
        /// RoketPack).
        /// </summary>
        internal static string OfficialRepository
        {
            get
            {
                return Manager.p_OfficialRepository;
            }
        }

        /// <summary>
        /// Returns the ID of the application 
        /// </summary>
        internal static string RoketPackID
        {
            get
            {
                return Manager.p_RoketPackID;
            }
        }

        /// <summary>
        /// Reinitalizes the repositories from the Repositories.xml on disk.
        /// </summary>
        internal void ReinitalizeRepositories()
        {
            this.InitalizeRepositories(true);
            if (this.p_MainForm != null)
            {
                VoidLambda lambda = () =>
                {
                    this.p_MainForm.ShowRepositoryReinitalization();
                };
                if (this.p_MainForm.InvokeRequired)
                    this.p_MainForm.Invoke(lambda);
                else
                    lambda();
            }
        }

        public override object InitializeLifetimeService()
        {
            return null;
        }

        internal bool RemoteURLInvoke(string url, bool trusted)
        {
            // We can't initalize the form because it would then
            // be created on the wrong thread.
            if (this.p_MainForm == null)
                return false;

            Manager.VoidLambda lambda = () =>
            {
                try
                {
                    this.p_MainForm.HandleURLNavigation(url, trusted);
                }
                catch (Exception ex)
                {
                    Errors.Raise(Errors.ErrorType.ERROR_CAN_NOT_HANDLE_URL_REQUEST);
                    return;
                }
            };

            this.p_MainForm.Invoke(lambda);
            return true;
        }
    }

    public class ImmediateExitException : ApplicationException
    {
        public ImmediateExitException() : base() { }
        public ImmediateExitException(string message) : base(message) { }
        public ImmediateExitException(string message, System.Exception inner) : base(message, inner) { }
    }
}
